import sys
import re
import shlex
import os
import wx
import wx.lib.agw.aui as aui
import wx.html
import firebrick
import textwrap
from robot.parsing.model import TestData
import wx.lib.mixins.listctrl as listmix
import rsa
from wx.lib.embeddedimage import PyEmbeddedImage
import thread
from contextlib import contextmanager
from rwb.process import Process, EVT_PROCESS_READABLE, EVT_PROCESS_ENDED

import wx.lib.agw.customtreectrl as customtreectrl

ID_FILTER_BY_NAME = wx.NewId()
ID_FILTER_BY_TESTCASE_TAGS = wx.NewId()
ID_FILTER_BY_ALL_TAGS = wx.NewId()
FILTER_NONE = "filter_none"
FILTER_NAME = "filter_name"
# perhaps we could filter based on the checked status...
# FILTER_CHECK = "filter_check"

FID = "rwb.core.rsa"

SETTING_CMD = "rsa_command"

class RSAPlugin(firebrick.Plugin):
    '''A plugin for working with the Robot Static Analyzer

    This view includes a checkbox next to each item so one could, for example,
    choose a subset of rules to create a new ruleset

    This is all very much a proof of concept at this point.
    '''

    def __init__(self, app):
        super(RSAPlugin, self).__init__(_id=FID,
                                        url=None, version="0.1", 
                                        name="Robot Static Analyzer",
                                        default_settings = {"enabled": []})

    def enable(self):
#        self.app.Register(RSANavigatorPanel)
#        self.app.Register(RSARuleViewerPanel)
        self.app.Register(RSALogPanel)
        self.app.Register(RSAPreferencesPanel)
        self.active = True

    def disable(self):
        self.active = False
#        self.app.Unregister(RSANavigatorPanel)
#        self.app.Unregister(RSARuleViewerPanel)
        self.app.Unregister(RSALogPanel)
        self.app.Unregister(RSAPreferencesPanel)

class RSARuleViewerPanel(wx.Panel):

    def __init__(self, parent):
        super(RSARuleViewerPanel, self).__init__(parent)
        self.window = parent

        self.htmlwin = wx.html.HtmlWindow(self, wx.ID_ANY)
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.htmlwin, 1, wx.EXPAND)
        self.SetSizer(sizer)



class CustomListCtrl(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin):
    def __init__(self, *args, **kwargs):
        super(CustomListCtrl, self).__init__(*args, **kwargs)
        # is the following necessary?
        listmix.ListCtrlAutoWidthMixin.__init__(self)
#        self.Bind(wx.EVT_MOTION, self.OnMouseMotion)

class RSALogPanel(firebrick.BasePanel):
    name = "RSA Log"
    _id = "rsa.log"
    container = "rwb.core.shelf_notebook"

    def __init__(self, parent):
        super(RSALogPanel, self).__init__(parent)
        self.window = parent
        self.rsa = rsa.validator.RSA()
        self.report = ()
        self.listctrl = CustomListCtrl(self, wx.ID_ANY, 
                                       style=wx.LC_REPORT)
        self.violations = []
        self.process = None
        self.listctrl.InsertColumn(0, "rule")
        self.listctrl.InsertColumn(1, "test name")
        self.listctrl.InsertColumn(2, "row")
        self.listctrl.InsertColumn(3, "message")

        self.listctrl.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnItemActivated)

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.listctrl, 1, wx.EXPAND)
        self.SetSizer(sizer)

        # This panel isn't managed by AUI, it's a tab in the
        # shelf plugin....
        # parent.auimgr.AddPane(self, aui.AuiPaneInfo().Bottom().
        #                       CaptionVisible(True).
        #                       Caption("RSA Log").
        #                       LeftDockable(False).
        #                       RightDockable(False).
        #                       TopDockable(False).
        #                       BottomDockable(True).
        #                       Dock().
        #                       BestSize(wx.Size(300,200)).
        #                       Name("rsa.log").Show(False))
        
#        suite = TestData(parent=None, source="/opt/orbitz-st/demos")


        self.window.Bind(EVT_PROCESS_READABLE, self.OnProcessReadable)
        self.window.Bind(EVT_PROCESS_ENDED, self.OnProcessEnded)

    def OnProcessReadable(self, event):
        print "readable"
        (stdout, stderr) = self.process.read()
        if stdout:
            # once RSA issue 21 gets implemented (--format option)
            # we won't have to work so hard to parse the output...
            print "=>", stdout
            match = re.search(r'\[(.*?)\]: *\[(.*?)\] *(.*?)\[(\d+)\]: *(.*?) *\| *(.*)', stdout)
            if match:
                index = self.listctrl.InsertStringItem(sys.maxint, label=match.group(2))
                self.listctrl.SetStringItem(index, 1, match.group(3))
                self.listctrl.SetStringItem(index, 2, match.group(4))
                self.listctrl.SetStringItem(index, 3, match.group(5))
                self.listctrl.SetItemData(index, len(self.violations)-1)
            else:
                # hmmm... what should I do here?
                pass
        if stderr: 
            # for now just ignore it. Probably need to log it 
            pass

    def OnProcessEnded(self, event):
        self.process = None

    def OnItemSelected(self, event):
        item = event.GetItem()
        violation = self.report.violations[item.GetData()]
        editor = self.window.GetPanel("rwb.core.edit_notebook")
        editor.Open(violation.test, target=violation.line)
        event.Skip()

    def OnItemActivated(self, event):
        item = event.GetItem()
        violation = self.violations[item.GetData()]
        editor = self.window.GetPanel("rwb.core.edit_notebook")
        editor.Open(violation.test, target=violation.line)
        event.Skip()

    def OnViolation(self, state=None, violation=None):
        if violation is not None:
            # OnViolation is called by the worker thread;
            # putting the data in the GUI must be done
            # by the main thread.
            wx.CallAfter(self._add_violation, violation)

    def _add_violation(self, violation):
        self.violations.append(violation)
        message = violation.rule.message + " " + str(violation.element)
        location = violation.test.name
        line = str(violation.line) or ""
        index = self.listctrl.InsertStringItem(sys.maxint, label=violation.rule.name)
        self.listctrl.SetStringItem(index, 1, location)
        self.listctrl.SetStringItem(index, 2, line)
        self.listctrl.SetStringItem(index, 3, message)
        self.listctrl.SetItemData(index, len(self.violations)-1)
        wx.Yield()
        
    def _worker_thread(self, suite):
        settings = wx.GetApp().GetSettings(FID)
#        self.rsa.ruleSets.use(settings["enabled"])
        self.report = self.rsa.validate(suite, callback=self.OnViolation)
        
    def Run(self, suite):
        self.window.ShowPanels("rsa.log")
        self.window.auimgr.Update()
        self.listctrl.DeleteAllItems()
        settings = wx.GetApp().GetSettings(FID)
        command = shlex.split(settings.get("rsa_command", "rsa"))
        command.append(suite.source)
        try:
            self.process = Process(command, target=self.window)
        except OSError as e:
            # it might be better to display this information in the RSA log
            # window rather than an annoying popup...
            dialog = wx.MessageDialog(self.window, 
                                      "There was a problem running the following command:\n\n" + 
                                      " ".join(command) + "\n\n" +
                                      "The command couldn't be found.",
                                      style=wx.OK|wx.ICON_INFORMATION)
            dialog.ShowModal()
            del dialog
        
    def oldRun(self, suite):
        self.window.ShowPanels("rsa.log")
#        self.window.LoadPanels("rsa.log")
        self.window.auimgr.Update()

        self.listctrl.DeleteAllItems()
        thread.start_new_thread(self._worker_thread,(suite,))

#        if not self.report.violations:
#            index=self.listctrl.InsertStringItem(sys.maxint, label="")
#            self.listctrl.SetStringItem(index, 3, "no violations found. Sweet.")
#            self.listctrl.SetColumnWidth(0, wx.LIST_AUTOSIZE)

class RSANavigatorPanel(wx.Panel):
    '''Panel with a tree for viewing rulesets and rules'''

    def __init__(self, parent, viewer):
        super(RSANavigatorPanel, self).__init__(parent)
        self.window = parent
        self.viewer = viewer
        self._rsa = rsa.validator.RSA()
        self._create_tree()
        self._change_events_enabled = True
        self.Filter(FILTER_NONE)

        self._apply_settings()

        # default filter type
        self.filter_type = FILTER_NAME

    def _check_all(self):
        for (obj, item) in self.node.iteritems():
            if isinstance(obj, rsa.rule.Rule):
                self.tree.CheckItem(item)

    def _apply_settings(self):
        '''Check all items according to the settings
        
        If settings are empty, check everything.
        '''
        with self.ChangeEventsDisabled():
            settings = wx.GetApp().GetSettings(FID)
            enabled = settings.get("enabled", [])
            check_all = (len(enabled) == 0)
            for (obj, item) in self.node.iteritems():
                if isinstance(obj, rsa.rule.Rule):
                    classname = obj.__class__.__name__
                    if check_all or classname in enabled:
                        self.tree.CheckItem(item)

    @contextmanager
    def ChangeEventsDisabled(self):
        '''Temporarily disable events that fire when the widget value changes'''
        orig_value = self._change_events_enabled
        self._change_events_enabled = False
        yield
        self._change_events_enabled = orig_value

    def _create_tree(self):
#        self.tree = customtreectrl.CustomTreeCtrl(self)
        style = customtreectrl.TR_HIDE_ROOT
        style |= customtreectrl.TR_HAS_BUTTONS
        style |= customtreectrl.TR_FULL_ROW_HIGHLIGHT

        self.tree = customtreectrl.CustomTreeCtrl(self, agwStyle=style)
        self._root = self.tree.AddRoot("Rule Sets")
        self.filter_menu = wx.Menu()
        # these need to be radio items rather than simple menu items
        item = self.filter_menu.Append(ID_FILTER_BY_NAME, 
                                       "Filter by name", kind=wx.ITEM_RADIO)
#        item = filter_menu.Append(ID_FILTER_BY_ALL_TAGS, "Filter by all tags")
        self.filter = wx.SearchCtrl(self, size=(200,-1), style=wx.TE_PROCESS_ENTER)
        self.filter.SetMenu(self.filter_menu)
        self.filter.SetDescriptiveText("filter by rule name")
        self.filter.ShowCancelButton(True)
        self.filter.ShowSearchButton(True)
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.tree, 1, wx.EXPAND)
        sizer.Add(self.filter, 0, wx.EXPAND|wx.TOP|wx.BOTTOM, 2)
        self.SetSizerAndFit(sizer)
        self.tree.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnTreeItemSelected)

        self.tree.Bind(customtreectrl.EVT_TREE_ITEM_CHECKED, self.OnCheck)
        self.filter.Bind(wx.EVT_SEARCHCTRL_CANCEL_BTN, self.OnCancelFilter)
        self.filter.Bind(wx.EVT_TEXT, self.OnFilter)
        self.filter.Bind(wx.EVT_TEXT_ENTER, self.OnFilter)
        self.filter.Bind(wx.EVT_MENU, self.OnFilterMenu)
        self.SetFilterType(ID_FILTER_BY_NAME)

    def _add_ruleset_node(self, ruleset):
        '''Add a node in the tree that represents a ruleset'''
        ruleset_node = self.tree.AppendItem(self._root, ruleset.name)
        self.tree.SetItemPyData(ruleset_node, ruleset)
        ruleset_node.Expand()
        self.node[ruleset] = ruleset_node
        
    def _add_rule_node(self, ruleset, rule):
        '''Add a node in the tree that represents a rule'''
        ruleset_node = self.node[ruleset]
        rule_node = self.tree.AppendItem(ruleset_node, rule.name, ct_type = 1)
        self.tree.SetItemPyData(rule_node, rule)
        self.node[rule] = rule_node
        
    def OnCheck(self, event):
        '''Called when an item is checked or unchecked

        This will save the current list of checked items
        in the settings file
        '''

        if not self._change_events_enabled: return

        checked = []
        for (obj, item) in self.node.iteritems():
            if isinstance(obj, rsa.rule.Rule) and item.IsChecked():
                # I don't like how I have to fetch the class name;
                # shouldn't there be an attribute I can get? 
                # perhaps obj.shortname?
                checked.append(obj.__class__.__name__)
        settings = wx.GetApp().GetSettings(FID)
        settings["enabled"] = checked
        wx.GetApp().SaveSettings()

    def OnCancelFilter(self, event):
        '''Called when the user presses cancel button in search control'''
        self.filter.SetValue("")
        self.Filter(FILTER_NONE, "")
        event.Skip()

    def OnFilterMenu(self, event):
        '''Called when user changes the filter type'''
        self.SetFilterType(event.GetId())
        event.Skip()

    def SetFilterType(self, filter_type):
        '''Filter_type must be one of the filter IDs (eg: ID_FILTER_BY_NAME)'''
        label = self.filter_menu.GetLabel(filter_type)
        self.filter.SetDescriptiveText(label)
        self.filter.SetToolTipString(label)
#        self.filter_menu.Check(filter_type, True)

        if filter_type == ID_FILTER_BY_NAME:
            self.filter_type = FILTER_NAME

    def OnFilter(self, event):
        self.Filter(self.filter_type, self.filter.GetValue())
        event.Skip()

    def View(self, obj):
        if isinstance(obj, rsa.rule.Rule):
            html = '''
                <h2>%s</h2>
                %s <!-- description -->
                <h3>Example</h3>
                <pre>%s</pre>
            ''' % (obj.name, obj.description, textwrap.dedent(obj.example))
            self.viewer.SetPage(html)
        elif isinstance(obj, rsa.ruleset.RuleSet):
            html = '<h2>Ruleset: %s</h2>\n' % obj.name
            html += "<p><blockquote>%s</blockquote></p>\n" % \
                obj.description.strip().replace("\n", "<br>\n")
            html += "<p><table>\n"
            color = {0: "#EBF3FF", 1: "#FFFFFF"}
            for row, rule in enumerate(obj.rules):
                html += "<tr bgcolor='%s'><td valign='top'>" % color[row%2]
                html += "%s</td><td>%s</td></tr>\n" % (
                    rule.name, rule.description)
            html += "</table></p>\n"
            self.viewer.SetPage(html)

    def Reset(self):
        self._root.Expand()
        self.tree.DeleteChildren(self._root)
        print "resetting self.node..."
        self.node = {}

    def Filter(self, filter_type, filter_pattern=""):
        '''Redraw only the rules that match the filter'''

        self.Reset()
        for ruleset in self._rsa.ruleSets:
            self._add_ruleset_node(ruleset)
#            ruleset_node = self.tree.AppendItem(self._root, ruleset.name)
#            self.tree.SetItemPyData(ruleset_node, ruleset)
#            ruleset_node.Expand()
            filter_pattern = filter_pattern.lower()
            for rule in ruleset:
                if ((filter_type == FILTER_NONE) or
                    (filter_type == FILTER_NAME and filter_pattern in rule.name.lower())):
                    self._add_rule_node(ruleset, rule)
#                    rule_node = self.tree.AppendItem(ruleset_node, rule.name, ct_type=1)
#                    self.tree.SetItemPyData(rule_node, rule)

    def OnTreeItemSelected(self, event):
        item = event.GetItem()
        if item:
            obj = self.tree.GetItemPyData(item)
            if obj:
                self.View(obj)

class RSAPreferencesPanel(firebrick.PreferencesPanel):
    location = ("Plugins","RSA",)
    title = "RSA - Robot Static Analyzer"

    def __init__(self, parent, *args, **kwargs):
        settings = wx.GetApp().GetSettings(FID)
        rsa_cmd = settings.setdefault(SETTING_CMD, "rsa")

        firebrick.PreferencesPanel.__init__(self, parent, *args, **kwargs)
#        sizer = wx.BoxSizer(wx.VERTICAL)
        rulebox = wx.StaticBox(self, wx.ID_ANY, "Rules")
        rulesizer = wx.StaticBoxSizer(rulebox)
        self.splitter = wx.SplitterWindow(self, wx.ID_ANY, style = wx.SP_LIVE_UPDATE)
        self.splitter.SetMinimumPaneSize(50)
        self.htmlwin = wx.html.HtmlWindow(self.splitter, wx.ID_ANY, size=(200,200))
        self.tree = RSANavigatorPanel(self.splitter, self.htmlwin)
        self.splitter.SplitVertically(self.tree, self.htmlwin, 100)
        rulesizer.Add(self.splitter, 1, wx.EXPAND|wx.ALL, 4)

        generalbox = wx.StaticBox(self, wx.ID_ANY, "General")
        generalsizer = wx.StaticBoxSizer(generalbox)
        exelabel = wx.StaticText(self, wx.ID_ANY, "RSA Command:")
        exeentry = wx.TextCtrl(self, wx.ID_ANY)
        exeentry.SetToolTip(wx.ToolTip("If rsa is on your path, 'rsa' is sufficient. Otherwise add a hard-coded path. You can include other arguments as well."))
        exeentry.AppendText(rsa_cmd)
        exeentry.Bind(wx.EVT_TEXT, self.OnCommandChanged)
        gridsizer = wx.FlexGridSizer(rows=1, cols=2)
        gridsizer.Add(exelabel, 1, wx.ALIGN_CENTER_VERTICAL)
        gridsizer.Add(exeentry, 1, wx.EXPAND|wx.LEFT, 8)
        gridsizer.AddGrowableCol(1, 1)
        generalsizer.Add(gridsizer, 1, wx.EXPAND)
        
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(generalsizer, 0, wx.EXPAND|wx.ALL, 4)
        sizer.Add(rulesizer, 1, wx.EXPAND|wx.ALL, 4)

        self.SetSizer(sizer)

        self._insert_welcome_message()

    def OnCommandChanged(self, event):
        cmd = event.GetEventObject().GetValue()
        app = wx.GetApp()
        settings = app.GetSettings(FID)
        settings[SETTING_CMD] = cmd
        app.SaveSettings()

    def _insert_welcome_message(self):
        # insert some useful welcome message in the html window
        # which will appear when nothing is selected.
        html = '''
          Clicking on a rule on the left will display documentation
          about that rule in this window. Clicking on a rule set
          will show a concise index of all the rules in this window.
          <p>
          When running RSA, any rules that are unchecked will be
          ignored. 
        '''
        self.htmlwin.SetPage(html)




RSA_image = PyEmbeddedImage(
    "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAABGdBTUEAAK/INwWK6QAAABl0"
    "RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAGSSURBVCjPVVFNSwJhEF78Ad79"
    "Cf6PvXQRsotUlzKICosuRYmR2RJR0KE6lBFFZVEbpFBSqKu2rum6llFS9HHI4iUhT153n6Zt"
    "IWMOM+/MM88z7wwH7s9Ub16SJcnbmrNcxVm2q7Z8/QPvEOtntpj92NkCqITLepEpjix7xQti"
    "LOoQ2b6+E7YAN/5nfOEJ2WbKqOIOJ4bYVMEQx4LfBBQDsvFMhUcCVU1/CxVXmDBGA5ZETrhD"
    "CQVcYAPbyEJBhvrnBVPiSpNr6cYDNCQwo4zzU/ySckkgDYuNuVpI42T9k4gLKGMPs/xPzzov"
    "QiY2hQYe0jlJfyNNhTqiWDYBq/wBMcSRpnyPzu1oS7WtxjVBSthU1vgVksiQ3Dn6Gp5ah2YO"
    "KQo5GiuHPA6xT1EKpxQNCNYejgIR457KKio0S56YckjSa9jo//3mrj+BV0QQagqGTOo+Y7gZ"
    "If1puP3WHoLhEb2PjTlCTCWGXtbp8DCX3hZuOdaIc9A+aQvWk4ihq95p67a7nP+u+Ws+r0dq"
    "l9z/zv0NCYhdCPKZ7oYAAAAASUVORK5CYII=")
getRSABitmap = RSA_image.GetBitmap
